/*
 * Copyright (c) 2009, Simon Berg
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
/*---------------------------------------------------------------------------*/
#include "contiki.h"

#include "strformat.h"
/*---------------------------------------------------------------------------*/
#define HAVE_DOUBLE
#define HAVE_LONGLONG

#ifndef LARGEST_SIGNED
#ifdef HAVE_LONGLONG
#define LARGEST_SIGNED long long int
#else
#define LARGEST_UNSIGNED long int
#endif /* HAVE_LONGLONG */
#endif /* LARGEST_SIGNED */

#ifndef LARGEST_UNSIGNED
#ifdef HAVE_LONGLONG
#define LARGEST_UNSIGNED unsigned long long int
#else
#define LARGEST_UNSIGNED unsigned long int
#endif /* HAVE_LONGLONG */
#endif /* LARGEST_UNSIGNED */

#ifndef POINTER_INT
#define POINTER_INT unsigned long
#endif
/*---------------------------------------------------------------------------*/
typedef unsigned int FormatFlags;
/*---------------------------------------------------------------------------*/
#define MAKE_MASK(shift, size) (((1 << size) - 1) << (shift))
/*---------------------------------------------------------------------------*/
#define JUSTIFY_SHIFT   0
#define JUSTIFY_SIZE    1
#define JUSTIFY_RIGHT   0x0000
#define JUSTIFY_LEFT    0x0001
#define JUSTIFY_MASK    MAKE_MASK(JUSTIFY_SHIFT, JUSTIFY_SIZE)
/*---------------------------------------------------------------------------*/
/* How a positive number is prefixed */
#define POSITIVE_SHIFT  (JUSTIFY_SHIFT + JUSTIFY_SIZE)
#define POSITIVE_NONE   (0x0000 << POSITIVE_SHIFT)
#define POSITIVE_SPACE  (0x0001 << POSITIVE_SHIFT)
#define POSITIVE_PLUS   (0x0003 << POSITIVE_SHIFT)
#define POSITIVE_MASK   MAKE_MASK(POSITIVE_SHIFT, POSITIVE_SIZE)

#define POSITIVE_SIZE   2
/*---------------------------------------------------------------------------*/
#define ALTERNATE_FORM_SHIFT (POSITIVE_SHIFT + POSITIVE_SIZE)
#define ALTERNATE_FORM_SIZE  1
#define ALTERNATE_FORM       (0x0001 << ALTERNATE_FORM_SHIFT)
/*---------------------------------------------------------------------------*/
#define PAD_SHIFT (ALTERNATE_FORM_SHIFT + ALTERNATE_FORM_SIZE)
#define PAD_SIZE  1
#define PAD_SPACE (0x0000 << PAD_SHIFT)
#define PAD_ZERO  (0x0001 << PAD_SHIFT)
/*---------------------------------------------------------------------------*/
#define SIZE_SHIFT    (PAD_SHIFT + PAD_SIZE)
#define SIZE_SIZE     3
#define SIZE_CHAR     (0x0001 << SIZE_SHIFT)
#define SIZE_SHORT    (0x0002 << SIZE_SHIFT)
#define SIZE_INT      (0x0000 << SIZE_SHIFT)
#define SIZE_LONG     (0x0003 << SIZE_SHIFT)
#define SIZE_LONGLONG (0x0004 << SIZE_SHIFT)
#define SIZE_MASK     MAKE_MASK(SIZE_SHIFT, SIZE_SIZE)
/*---------------------------------------------------------------------------*/
#define CONV_SHIFT    (SIZE_SHIFT + SIZE_SIZE)
#define CONV_SIZE     3
#define CONV_INTEGER  (0x0001 << CONV_SHIFT)
#define CONV_FLOAT    (0x0002 << CONV_SHIFT)
#define CONV_POINTER  (0x0003 << CONV_SHIFT)
#define CONV_STRING   (0x0004 << CONV_SHIFT)
#define CONV_CHAR     (0x0005 << CONV_SHIFT)
#define CONV_PERCENT  (0x0006 << CONV_SHIFT)
#define CONV_WRITTEN  (0x0007 << CONV_SHIFT)
#define CONV_MASK     MAKE_MASK(CONV_SHIFT, CONV_SIZE)
/*---------------------------------------------------------------------------*/
#define RADIX_SHIFT   (CONV_SHIFT + CONV_SIZE)
#define RADIX_SIZE    2
#define RADIX_DECIMAL (0x0001 << RADIX_SHIFT)
#define RADIX_OCTAL   (0x0002 << RADIX_SHIFT)
#define RADIX_HEX     (0x0003 << RADIX_SHIFT)
#define RADIX_MASK    MAKE_MASK(RADIX_SHIFT, RADIX_SIZE)
/*---------------------------------------------------------------------------*/
#define SIGNED_SHIFT  (RADIX_SHIFT + RADIX_SIZE)
#define SIGNED_SIZE   1
#define SIGNED_NO     (0x0000 << SIGNED_SHIFT)
#define SIGNED_YES    (0x0001 << SIGNED_SHIFT)
#define SIGNED_MASK   MAKE_MASK(SIGNED_SHIFT, SIGNED_SIZE)
/*---------------------------------------------------------------------------*/
#define CAPS_SHIFT  (SIGNED_SHIFT + SIGNED_SIZE)
#define CAPS_SIZE 1
#define CAPS_NO     (0x0000 << CAPS_SHIFT)
#define CAPS_YES    (0x0001 << CAPS_SHIFT)
#define CAPS_MASK   MAKE_MASK(CAPS_SHIFT, CAPS_SIZE)
/*---------------------------------------------------------------------------*/
#define FLOAT_SHIFT     (CAPS_SHIFT + CAPS_SIZE)
#define FLOAT_SIZE      2
#define FLOAT_NORMAL    (0x0000 << FLOAT_SHIFT)
#define FLOAT_EXPONENT  (0x0001 << FLOAT_SHIFT)
#define FLOAT_DEPENDANT (0x0002 << FLOAT_SHIFT)
#define FLOAT_HEX       (0x0003 << FLOAT_SHIFT)
#define FLOAT_MASK      MAKE_MASK(FLOAT_SHIFT, FLOAT_SIZE)
/*---------------------------------------------------------------------------*/
#define CHECKCB(res) { if((res) != STRFORMAT_OK) { va_end(ap); return -1; } }
/*---------------------------------------------------------------------------*/
#define MAXCHARS_HEX ((sizeof(LARGEST_UNSIGNED) * 8) / 4)

/* Largest number of characters needed for converting an unsigned integer. */
#define MAXCHARS ((sizeof(LARGEST_UNSIGNED) * 8 + 2) / 3)
/*---------------------------------------------------------------------------*/
static FormatFlags
parse_flags(const char **posp)
{
  FormatFlags flags = 0;
  const char *pos = *posp;

  while(1) {
    switch(*pos) {
    case '-':
      flags |= JUSTIFY_LEFT;
      break;
    case '+':
      flags |= POSITIVE_PLUS;
      break;
    case ' ':
      flags |= POSITIVE_SPACE;
      break;
    case '#':
      flags |= ALTERNATE_FORM;
      break;
    case '0':
      flags |= PAD_ZERO;
      break;
    default:
      *posp = pos;
      return flags;
    }

    pos++;
  }
}
/*---------------------------------------------------------------------------*/
static unsigned int
parse_uint(const char **posp)
{
  unsigned v = 0;
  const char *pos = *posp;
  char ch;

  while((ch = *pos) >= '0' && ch <= '9') {
    v = v * 10 + (ch - '0');
    pos++;
  }

  *posp = pos;

  return v;
}
/*---------------------------------------------------------------------------*/
static unsigned int
output_uint_decimal(char **posp, LARGEST_UNSIGNED v)
{
  unsigned int len;
  char *pos = *posp;

  while(v > 0) {
    *--pos = (v % 10) + '0';
    v /= 10;
  }

  len = *posp - pos;
  *posp = pos;

  return len;
}
/*---------------------------------------------------------------------------*/
static unsigned int
output_uint_hex(char **posp, LARGEST_UNSIGNED v, unsigned int flags)
{
  unsigned int len;
  const char *hex = (flags & CAPS_YES) ? "0123456789ABCDEF" : "0123456789abcdef";
  char *pos = *posp;

  while(v > 0) {
    *--pos = hex[(v % 16)];
    v /= 16;
  }

  len = *posp - pos;
  *posp = pos;

  return len;
}
/*---------------------------------------------------------------------------*/
static unsigned int
output_uint_octal(char **posp, LARGEST_UNSIGNED v)
{
  unsigned int len;
  char *pos = *posp;

  while(v > 0) {
    *--pos = (v % 8) + '0';
    v /= 8;
  }

  len = *posp - pos;
  *posp = pos;

  return len;
}
/*---------------------------------------------------------------------------*/
static strformat_result
fill_space(const strformat_context_t *ctxt, unsigned int len)
{
  strformat_result res;
  static const char buffer[16] = "                ";

  while(len > 16) {
    res = ctxt->write_str(ctxt->user_data, buffer, 16);
    if(res != STRFORMAT_OK) {
      return res;
    }
    len -= 16;
  }

  if(len == 0) {
    return STRFORMAT_OK;
  }

  return ctxt->write_str(ctxt->user_data, buffer, len);
}
/*---------------------------------------------------------------------------*/
static strformat_result
fill_zero(const strformat_context_t *ctxt, unsigned int len)
{
  strformat_result res;
  static const char buffer[16] = "0000000000000000";

  while(len > 16) {
    res = ctxt->write_str(ctxt->user_data, buffer, 16);
    if(res != STRFORMAT_OK) {
      return res;
    }
    len -= 16;
  }

  if(len == 0) {
    return STRFORMAT_OK;
  }
  return ctxt->write_str(ctxt->user_data, buffer, len);
}
/*---------------------------------------------------------------------------*/
int
format_str(const strformat_context_t *ctxt, const char *format, ...)
{
  int ret;
  va_list ap;
  va_start(ap, format);
  ret = format_str_v(ctxt, format, ap);
  va_end(ap);
  return ret;
}
/*---------------------------------------------------------------------------*/
int
format_str_v(const strformat_context_t *ctxt, const char *format, va_list ap)
{
  unsigned int written = 0;
  const char *pos = format;

  while(*pos != '\0') {
    FormatFlags flags;
    unsigned int minwidth = 0;
    int precision = -1; /* Negative means no precision */
    char ch;
    const char *start = pos;

    while((ch = *pos) != '\0' && ch != '%') {
      pos++;
    }

    if(pos != start) {
      CHECKCB(ctxt->write_str(ctxt->user_data, start, pos - start));
      written += pos - start;
    }

    if(*pos == '\0') {
      va_end(ap);
      return written;
    }

    pos++;

    if(*pos == '\0') {
      va_end(ap);
      return written;
    }

    flags = parse_flags(&pos);

    /* parse width */
    if(*pos >= '1' && *pos <= '9') {
      minwidth = parse_uint(&pos);
    } else if(*pos == '*') {
      int w = va_arg(ap, int);

      if(w < 0) {
        flags |= JUSTIFY_LEFT;
        minwidth = w;
      } else {
        minwidth = w;
      }

      pos++;
    }

    /* parse precision */
    if(*pos == '.') {
      pos++;

      if(*pos >= '0' && *pos <= '9') {
        precision = parse_uint(&pos);
      } else if(*pos == '*') {
        pos++;
        precision = va_arg(ap, int);
      }
    }

    if(*pos == 'l') {
      pos++;

      if(*pos == 'l') {
        flags |= SIZE_LONGLONG;
        pos++;
      } else {
        flags |= SIZE_LONG;
      }
    } else if(*pos == 'h') {
      pos++;

      if(*pos == 'h') {
        flags |= SIZE_CHAR;
        pos++;
      } else {
        flags |= SIZE_SHORT;
      }
    } else if(*pos == 'z') {
      if(sizeof(size_t) == sizeof(short)) {
        flags |= SIZE_SHORT;
      } else if(sizeof(size_t) == sizeof(long)) {
        flags |= SIZE_LONG;
#ifdef HAVE_LONGLONG
      } else if(sizeof(size_t) == sizeof(long long)) {
        flags |= SIZE_LONGLONG;
      }
#endif
      pos++;
    }

    /* parse conversion specifier */
    switch(*pos) {
    case 'd':
    case 'i':
      flags |= CONV_INTEGER | RADIX_DECIMAL | SIGNED_YES;
      break;
    case 'u':
      flags |= CONV_INTEGER | RADIX_DECIMAL | SIGNED_NO;
      break;
    case 'o':
      flags |= CONV_INTEGER | RADIX_OCTAL | SIGNED_NO;
      break;
    case 'x':
      flags |= CONV_INTEGER | RADIX_HEX | SIGNED_NO;
      break;
    case 'X':
      flags |= CONV_INTEGER | RADIX_HEX | SIGNED_NO | CAPS_YES;
      break;
#ifdef HAVE_DOUBLE
    case 'f':
      flags |= CONV_FLOAT | FLOAT_NORMAL;
      break;
    case 'F':
      flags |= CONV_FLOAT | FLOAT_NORMAL | CAPS_YES;
      break;
    case 'e':
      flags |= CONV_FLOAT | FLOAT_EXPONENT;
      break;
    case 'E':
      flags |= CONV_FLOAT | FLOAT_EXPONENT | CAPS_YES;
      break;
    case 'g':
      flags |= CONV_FLOAT | FLOAT_DEPENDANT;
      break;
    case 'G':
      flags |= CONV_FLOAT | FLOAT_DEPENDANT | CAPS_YES;
      break;
    case 'a':
      flags |= CONV_FLOAT | FLOAT_HEX;
      break;
    case 'A':
      flags |= CONV_FLOAT | FLOAT_HEX | CAPS_YES;
      break;
#endif
    case 'c':
      flags |= CONV_CHAR;
      break;
    case 's':
      flags |= CONV_STRING;
      break;
    case 'p':
      flags |= CONV_POINTER;
      break;
    case 'n':
      flags |= CONV_WRITTEN;
      break;
    case '%':
      flags |= CONV_PERCENT;
      break;
    case '\0':
      va_end(ap);
      return written;
    }
    pos++;

    switch(flags & CONV_MASK) {
    case CONV_PERCENT:
      CHECKCB(ctxt->write_str(ctxt->user_data, "%", 1));
      written++;
      break;
    case CONV_INTEGER:
    {
      /* unsigned integers */
      char *prefix = 0; /* sign, "0x" or "0X" */
      unsigned int prefix_len = 0;
      char buffer[MAXCHARS];
      char *conv_pos = buffer + MAXCHARS;
      unsigned int conv_len = 0;
      unsigned int width = 0;
      unsigned int precision_fill;
      unsigned int field_fill;
      LARGEST_UNSIGNED uvalue = 0;
      int negative = 0;

      if(precision < 0) {
        precision = 1;
      } else {
        flags &= ~PAD_ZERO;
      }

      if(flags & SIGNED_YES) {
        /* signed integers */
        LARGEST_SIGNED value = 0;
        switch(flags & SIZE_MASK) {
        case SIZE_CHAR:
          value = (signed char)va_arg(ap, int);
          break;
        case SIZE_SHORT:
          value = (short)va_arg(ap, int);
          break;
        case SIZE_INT:
          value = va_arg(ap, int);
          break;
#ifndef HAVE_LONGLONG
        case SIZE_LONGLONG: /* Treat long long the same as long */
#endif
        case SIZE_LONG:
          value = va_arg(ap, long);
          break;
#ifdef HAVE_LONGLONG
        case SIZE_LONGLONG:
          value = va_arg(ap, long long);
          break;
#endif
        }
        if(value < 0) {
          uvalue = -value;
          negative = 1;
        } else {
          uvalue = value;
        }
      } else {

        switch(flags & SIZE_MASK) {
        case SIZE_CHAR:
          uvalue = (unsigned char)va_arg(ap, unsigned int);
          break;
        case SIZE_SHORT:
          uvalue = (unsigned short)va_arg(ap, unsigned int);
          break;
        case SIZE_INT:
          uvalue = va_arg(ap, unsigned int);
          break;
#ifndef HAVE_LONGLONG
        case SIZE_LONGLONG: /* Treat long long the same as long */
#endif
        case SIZE_LONG:
          uvalue = va_arg(ap, unsigned long);
          break;
#ifdef HAVE_LONGLONG
        case SIZE_LONGLONG:
          uvalue = va_arg(ap, unsigned long long);
          break;
#endif
        }
      }

      switch(flags & (RADIX_MASK)) {
      case RADIX_DECIMAL:
        conv_len = output_uint_decimal(&conv_pos, uvalue);
        break;
      case RADIX_OCTAL:
        conv_len = output_uint_octal(&conv_pos, uvalue);
        break;
      case RADIX_HEX:
        conv_len = output_uint_hex(&conv_pos, uvalue, flags);
        break;
      }

      width += conv_len;
      precision_fill = (precision > conv_len) ? precision - conv_len : 0;
      if((flags & (RADIX_MASK | ALTERNATE_FORM))
         == (RADIX_OCTAL | ALTERNATE_FORM)) {
        if(precision_fill < 1) {
          precision_fill = 1;
        }
      }

      width += precision_fill;

      if((flags & (RADIX_MASK | ALTERNATE_FORM))
         == (RADIX_HEX | ALTERNATE_FORM) && uvalue != 0) {
        prefix_len = 2;
        if(flags & CAPS_YES) {
          prefix = "0X";
        } else {
          prefix = "0x";
        }
      }

      if(flags & SIGNED_YES) {
        if(negative) {
          prefix = "-";
          prefix_len = 1;
        } else {
          switch(flags & POSITIVE_MASK) {
          case POSITIVE_SPACE:
            prefix = " ";
            prefix_len = 1;
            break;
          case POSITIVE_PLUS:
            prefix = "+";
            prefix_len = 1;
            break;
          }
        }
      }

      width += prefix_len;

      field_fill = (minwidth > width) ? minwidth - width : 0;

      if((flags & JUSTIFY_MASK) == JUSTIFY_RIGHT) {
        if(flags & PAD_ZERO) {
          precision_fill += field_fill;
          field_fill = 0; /* Do not double count padding */
        } else {
          CHECKCB(fill_space(ctxt, field_fill));
        }
      }

      if(prefix_len > 0) {
        CHECKCB(ctxt->write_str(ctxt->user_data, prefix, prefix_len));
      }
      written += prefix_len;

      CHECKCB(fill_zero(ctxt, precision_fill));
      written += precision_fill;

      CHECKCB(ctxt->write_str(ctxt->user_data, conv_pos, conv_len));
      written += conv_len;

      if((flags & JUSTIFY_MASK) == JUSTIFY_LEFT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }
      written += field_fill;
    }
    break;
    case CONV_STRING:
    {
      unsigned int field_fill;
      unsigned int len;
      char *str = va_arg(ap, char *);

      if(str) {
        char *pos = str;
        while(*pos != '\0') pos++;
        len = pos - str;
      } else {
        str = "(null)";
        len = 6;
      }

      if(precision >= 0 && precision < len) {
        len = precision;
      }

      field_fill = (minwidth > len) ? minwidth - len : 0;

      if((flags & JUSTIFY_MASK) == JUSTIFY_RIGHT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }

      CHECKCB(ctxt->write_str(ctxt->user_data, str, len));
      written += len;

      if((flags & JUSTIFY_MASK) == JUSTIFY_LEFT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }
      written += field_fill;
    }
    break;
    case CONV_POINTER:
    {
      LARGEST_UNSIGNED uvalue =
        (LARGEST_UNSIGNED)(POINTER_INT)va_arg(ap, void *);
      char buffer[MAXCHARS_HEX + 3];
      char *conv_pos = buffer + MAXCHARS_HEX + 3;
      unsigned int conv_len;
      unsigned int field_fill;

      conv_len = output_uint_hex(&conv_pos, uvalue, flags);

      if(conv_len == 0) {
        *--conv_pos = '0';
        conv_len++;
      }

      *--conv_pos = 'x';
      *--conv_pos = '0';
      *--conv_pos = '#';
      conv_len += 3;

      field_fill = (minwidth > conv_len) ? minwidth - conv_len : 0;

      if((flags & JUSTIFY_MASK) == JUSTIFY_RIGHT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }

      CHECKCB(ctxt->write_str(ctxt->user_data, conv_pos, conv_len));
      written += conv_len;

      if((flags & JUSTIFY_MASK) == JUSTIFY_LEFT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }

      written += field_fill;
    }
    break;
    case CONV_CHAR:
    {
      char ch = va_arg(ap, int);
      unsigned int field_fill = (minwidth > 1) ? minwidth - 1 : 0;

      if((flags & JUSTIFY_MASK) == JUSTIFY_RIGHT) {
        CHECKCB(fill_space(ctxt, field_fill));
        written += field_fill;
      }

      CHECKCB(ctxt->write_str(ctxt->user_data, &ch, 1));
      written++;

      if((flags & JUSTIFY_MASK) == JUSTIFY_LEFT) {
        CHECKCB(fill_space(ctxt, field_fill));
      }
      written += field_fill;
    }
    break;
    case CONV_WRITTEN:
    {
      int *p = va_arg(ap, int *);
      *p = written;
    }
    break;
    }
  }

  return written;
}
/*---------------------------------------------------------------------------*/
